 /*******************************************************************************
  * Copyright (c) 2005, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.core.commands.operations;

 import java.util.ArrayList ;
 import java.util.List ;

 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.core.runtime.IAdaptable;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.OperationCanceledException;
 import org.eclipse.core.runtime.Status;

 /**
  * Triggered operations are a specialized implementation of a composite
  * operation that keeps track of operations triggered by the execution of some
  * primary operation. The composite knows which operation was the trigger for
  * subsequent operations, and adds all triggered operations as children. When
  * execution, undo, or redo is performed, only the triggered operation is
  * executed, undone, or redone if it is still present. If the trigger is removed
  * from the triggered operations, then the child operations will replace the
  * triggered operations in the history.
  * <p>
  * This class may be instantiated by clients.
  * </p>
  *
  * @since 3.1
  */
 public final class TriggeredOperations extends AbstractOperation implements
         ICompositeOperation, IAdvancedUndoableOperation,
         IContextReplacingOperation {

     private IUndoableOperation triggeringOperation;

     private IOperationHistory history;

     private List children = new ArrayList ();

     /**
      * Construct a composite triggered operations using the specified undoable
      * operation as the trigger. Use the label of this trigger as the label of
      * the operation.
      *
      * @param operation
      * the operation that will trigger other operations.
      * @param history
      * the operation history containing the triggered operations.
      */
     public TriggeredOperations(IUndoableOperation operation,
             IOperationHistory history) {
         super(operation.getLabel());
         triggeringOperation = operation;
         recomputeContexts();
         this.history = history;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.commands.operations.IUndoableOperation#add(org.eclipse.core.commands.operations.IUndoableOperation)
      */
     public void add(IUndoableOperation operation) {
         children.add(operation);
         recomputeContexts();
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.commands.operations.IUndoableOperation#remove(org.eclipse.core.commands.operations.IUndoableOperation)
      */
     public void remove(IUndoableOperation operation) {
         if (operation == triggeringOperation) {
             // the triggering operation is being removed, so we must replace
 // this composite with its individual triggers.
 triggeringOperation = null;
             // save the children before replacing the operation, since this
 // operation will be disposed as part of replacing it. We don't want
 // the children to be disposed since they are to replace this
 // operation.
 List childrenToRestore = new ArrayList (children);
             children = new ArrayList (0);
             recomputeContexts();
             operation.dispose();
             // now replace the triggering operation
 history.replaceOperation(this,
                     (IUndoableOperation[]) childrenToRestore
                             .toArray(new IUndoableOperation[childrenToRestore
                                     .size()]));
         } else {
             children.remove(operation);
             operation.dispose();
             recomputeContexts();
         }
     }

     /**
      * Remove the specified context from the receiver. This method is typically
      * invoked when the history is being flushed for a certain context. In the
      * case of triggered operations, if the only context for the triggering
      * operation is being removed, then the triggering operation must be
      * replaced in the operation history with the atomic operations that it
      * triggered. If the context being removed is not the only context for the
      * triggering operation, the triggering operation will remain, and the
      * children will each be similarly checked.
      *
      * @param context
      * the undo context being removed from the receiver.
      */
     public void removeContext(IUndoContext context) {

         boolean recompute = false;
         // first check to see if we are removing the only context of the
 // triggering operation
 if (triggeringOperation != null
                 && triggeringOperation.hasContext(context)) {
             if (triggeringOperation.getContexts().length == 1) {
                 remove(triggeringOperation);
                 return;
             }
             triggeringOperation.removeContext(context);
             recompute = true;
         }
         // the triggering operation remains, check all the children
 ArrayList toBeRemoved = new ArrayList ();
         for (int i = 0; i < children.size(); i++) {
             IUndoableOperation child = (IUndoableOperation) children.get(i);
             if (child.hasContext(context)) {
                 if (child.getContexts().length == 1) {
                     toBeRemoved.add(child);
                 } else {
                     child.removeContext(context);
                 }
                 recompute = true;
             }
         }
         for (int i = 0; i < toBeRemoved.size(); i++) {
             remove((IUndoableOperation) toBeRemoved.get(i));
         }
         if (recompute) {
             recomputeContexts();
         }
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.commands.operations.IUndoableOperation#execute(org.eclipse.core.runtime.IProgressMonitor,
      * org.eclipse.core.runtime.IAdaptable)
      */
     public IStatus execute(IProgressMonitor monitor, IAdaptable info)
             throws ExecutionException {
         if (triggeringOperation != null) {
             history.openOperation(this, IOperationHistory.EXECUTE);
             try {
                 IStatus status = triggeringOperation.execute(monitor, info);
                 history.closeOperation(status.isOK(), false,
                         IOperationHistory.EXECUTE);
                 return status;
             } catch (ExecutionException e) {
                 history.closeOperation(false, false, IOperationHistory.EXECUTE);
                 throw e;
             } catch (RuntimeException e) {
                 history.closeOperation(false, false, IOperationHistory.EXECUTE);
                 throw e;
             }

         }
         return IOperationHistory.OPERATION_INVALID_STATUS;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.commands.operations.IUndoableOperation#redo(org.eclipse.core.runtime.IProgressMonitor,
      * org.eclipse.core.runtime.IAdaptable)
      */
     public IStatus redo(IProgressMonitor monitor, IAdaptable info)
             throws ExecutionException {
         if (triggeringOperation != null) {
             history.openOperation(this, IOperationHistory.REDO);
             List childrenToRestore = new ArrayList (children);
             try {
                 removeAllChildren();
                 IStatus status = triggeringOperation.redo(monitor, info);
                 if (!status.isOK()) {
                     children = childrenToRestore;
                 }
                 history.closeOperation(status.isOK(), false,
                         IOperationHistory.REDO);
                 return status;
             } catch (ExecutionException e) {
                 children = childrenToRestore;
                 history.closeOperation(false, false, IOperationHistory.REDO);
                 throw e;
             } catch (RuntimeException e) {
                 children = childrenToRestore;
                 history.closeOperation(false, false, IOperationHistory.REDO);
                 throw e;
             }
         }
         return IOperationHistory.OPERATION_INVALID_STATUS;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.commands.operations.IUndoableOperation#undo(org.eclipse.core.runtime.IProgressMonitor,
      * org.eclipse.core.runtime.IAdaptable)
      */
     public IStatus undo(IProgressMonitor monitor, IAdaptable info)
             throws ExecutionException {
         if (triggeringOperation != null) {
             history.openOperation(this, IOperationHistory.UNDO);
             List childrenToRestore = new ArrayList (children);
             try {
                 removeAllChildren();
                 IStatus status = triggeringOperation.undo(monitor, info);
                 if (!status.isOK()) {
                     children = childrenToRestore;
                 }
                 history.closeOperation(status.isOK(), false,
                         IOperationHistory.UNDO);
                 return status;
             } catch (ExecutionException e) {
                 children = childrenToRestore;
                 history.closeOperation(false, false, IOperationHistory.UNDO);
                 throw e;
             } catch (RuntimeException e) {
                 children = childrenToRestore;
                 history.closeOperation(false, false, IOperationHistory.UNDO);
                 throw e;
             }
         }
         return IOperationHistory.OPERATION_INVALID_STATUS;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.commands.operations.IUndoableOperation#canUndo()
      */
     public boolean canUndo() {
         if (triggeringOperation != null) {
             return triggeringOperation.canUndo();
         }
         return false;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.commands.operations.IUndoableOperation#canExecute()
      */
     public boolean canExecute() {
         if (triggeringOperation != null) {
             return triggeringOperation.canExecute();
         }
         return false;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.commands.operations.IUndoableOperation#canRedo()
      */
     public boolean canRedo() {
         if (triggeringOperation != null) {
             return triggeringOperation.canRedo();
         }
         return false;
     }

     /*
      * Dispose all operations in the receiver.
      */
     public void dispose() {
         for (int i = 0; i < children.size(); i++) {
             ((IUndoableOperation) (children.get(i))).dispose();
         }
         if (triggeringOperation != null) {
             triggeringOperation.dispose();
         }
     }

     /*
      * Recompute contexts in light of some change in the children
      */
     private void recomputeContexts() {
         ArrayList allContexts = new ArrayList ();
         if (triggeringOperation != null) {
             IUndoContext[] contexts = triggeringOperation.getContexts();
             for (int i = 0; i < contexts.length; i++) {
                 allContexts.add(contexts[i]);
             }
         }
         for (int i = 0; i < children.size(); i++) {
             IUndoContext[] contexts = ((IUndoableOperation) children.get(i))
                     .getContexts();
             for (int j = 0; j < contexts.length; j++) {
                 if (!allContexts.contains(contexts[j])) {
                     allContexts.add(contexts[j]);
                 }
             }
         }
         contexts = allContexts;

     }

     /*
      * Remove all non-triggering children
      */
     private void removeAllChildren() {
         IUndoableOperation[] nonTriggers = (IUndoableOperation[]) children
                 .toArray(new IUndoableOperation[children.size()]);
         for (int i = 0; i < nonTriggers.length; i++) {
             children.remove(nonTriggers[i]);
             nonTriggers[i].dispose();
         }
     }

     /**
      * Return the operation that triggered the other operations in this
      * composite.
      *
      * @return the IUndoableOperation that triggered the other children.
      */
     public IUndoableOperation getTriggeringOperation() {
         return triggeringOperation;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.commands.operations.IAdvancedModelOperation#getAffectedObjects()
      */
     public Object [] getAffectedObjects() {
         if (triggeringOperation instanceof IAdvancedUndoableOperation) {
             return ((IAdvancedUndoableOperation) triggeringOperation)
                     .getAffectedObjects();
         }
         return null;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.commands.operations.IAdvancedModelOperation#aboutToNotify(org.eclipse.core.commands.operations.OperationHistoryEvent)
      */
     public void aboutToNotify(OperationHistoryEvent event) {
         if (triggeringOperation instanceof IAdvancedUndoableOperation) {
             ((IAdvancedUndoableOperation) triggeringOperation)
                     .aboutToNotify(event);
         }
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.commands.operations.IAdvancedUndoableOperation#computeUndoableStatus(org.eclipse.core.runtime.IProgressMonitor)
      */
     public IStatus computeUndoableStatus(IProgressMonitor monitor)
             throws ExecutionException {
         if (triggeringOperation instanceof IAdvancedUndoableOperation) {
             try {
                 return ((IAdvancedUndoableOperation) triggeringOperation)
                         .computeUndoableStatus(monitor);
             } catch (OperationCanceledException e) {
                 return Status.CANCEL_STATUS;
             }
         }
         return Status.OK_STATUS;

     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.commands.operations.IAdvancedUndoableOperation#computeRedoableStatus(org.eclipse.core.runtime.IProgressMonitor)
      */
     public IStatus computeRedoableStatus(IProgressMonitor monitor)
             throws ExecutionException {
         if (triggeringOperation instanceof IAdvancedUndoableOperation) {
             try {
                 return ((IAdvancedUndoableOperation) triggeringOperation)
                         .computeRedoableStatus(monitor);
             } catch (OperationCanceledException e) {
                 return Status.CANCEL_STATUS;
             }
         }
         return Status.OK_STATUS;

     }

     /**
      * Replace the undo context of the receiver with the provided replacement
      * undo context. In the case of triggered operations, all contained
      * operations are checked and any occurrence of the original context is
      * replaced with the new undo context.
      * <p>
      * This message has no effect if the original undo context is not present in
      * the receiver.
      *
      * @param original
      * the undo context which is to be replaced
      * @param replacement
      * the undo context which is replacing the original
      * @since 3.2
      */
     public void replaceContext(IUndoContext original, IUndoContext replacement) {

         // first check the triggering operation
 if (triggeringOperation != null
                 && triggeringOperation.hasContext(original)) {
             if (triggeringOperation instanceof IContextReplacingOperation) {
                 ((IContextReplacingOperation) triggeringOperation)
                         .replaceContext(original, replacement);
             } else {
                 triggeringOperation.removeContext(original);
                 triggeringOperation.addContext(replacement);
             }
         }
         // Now check all the children
 for (int i = 0; i < children.size(); i++) {
             IUndoableOperation child = (IUndoableOperation) children.get(i);
             if (child.hasContext(original)) {
                 if (child instanceof IContextReplacingOperation) {
                     ((IContextReplacingOperation) child).replaceContext(
                             original, replacement);
                 } else {
                     child.removeContext(original);
                     child.addContext(replacement);
                 }
             }
         }
         recomputeContexts();
     }

     /**
      * Add the specified context to the operation. Overridden in
      * TriggeredOperations to add the specified undo context to the triggering
      * operation.
      *
      * @param context
      * the context to be added
      *
      * @since 3.2
      */
     public void addContext(IUndoContext context) {
         if (triggeringOperation != null) {
             triggeringOperation.addContext(context);
             recomputeContexts();
         }
     }

 }

